import os
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from docx import Document
import threading
import time
import win32com.client
import pythoncom

# 软件信息常量
APP_NAME = "文档文本替换工具"
APP_VERSION = "1.0.0"
APP_AUTHOR = "陈良旭"
APP_COPYRIGHT = "© 2025 chenliangxu68@163.com"
APP_DESCRIPTION = "一个批量替换文件夹中的doc和docx文档中文字的工具。"

class DocReplacer:
    def __init__(self, root):
        self.root = root
        self.root.title(APP_NAME)
        self.root.geometry("800x600")
        self.root.resizable(True, True)
        
        # 创建菜单栏
        self.create_menu_bar()

        # 创建主框架
        self.main_frame = tk.Frame(self.root, padx=10, pady=10)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建选择文件夹部分
        self.folder_frame = tk.LabelFrame(self.main_frame, text="选择文件夹", padx=5, pady=5)
        self.folder_frame.pack(fill=tk.X, pady=5)

        self.folder_path = tk.StringVar()
        self.folder_entry = tk.Entry(self.folder_frame, textvariable=self.folder_path, width=70)
        self.folder_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

        self.browse_button = tk.Button(self.folder_frame, text="浏览", command=self.browse_folder)
        self.browse_button.pack(side=tk.RIGHT, padx=5)

        # 创建替换文本部分
        self.replace_frame = tk.LabelFrame(self.main_frame, text="替换文本", padx=5, pady=5)
        self.replace_frame.pack(fill=tk.X, pady=5)

        tk.Label(self.replace_frame, text="查找文本:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.find_text = tk.Entry(self.replace_frame, width=50)
        self.find_text.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W+tk.E)

        tk.Label(self.replace_frame, text="替换为:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        self.replace_text = tk.Entry(self.replace_frame, width=50)
        self.replace_text.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W+tk.E)

        self.replace_frame.columnconfigure(1, weight=1)

        # 创建文件类型选择部分
        self.filetype_frame = tk.LabelFrame(self.main_frame, text="文件类型选择", padx=5, pady=5)
        self.filetype_frame.pack(fill=tk.X, pady=5)
        
        self.docx_var = tk.BooleanVar(value=True)
        self.doc_var = tk.BooleanVar(value=True)
        
        self.docx_check = tk.Checkbutton(self.filetype_frame, text="DOCX 文件 (.docx)", variable=self.docx_var)
        self.docx_check.pack(anchor=tk.W, padx=5, pady=2)
        
        self.doc_check = tk.Checkbutton(self.filetype_frame, text="DOC 文件 (.doc)", variable=self.doc_var)
        self.doc_check.pack(anchor=tk.W, padx=5, pady=2)

        # 创建操作按钮部分
        self.button_frame = tk.Frame(self.main_frame)
        self.button_frame.pack(fill=tk.X, pady=5)

        self.replace_button = tk.Button(self.button_frame, text="开始替换", command=self.start_replace)
        self.replace_button.pack(side=tk.RIGHT, padx=5)

        # 创建进度显示部分
        self.progress_frame = tk.LabelFrame(self.main_frame, text="处理进度", padx=5, pady=5)
        self.progress_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.progress_text = scrolledtext.ScrolledText(self.progress_frame, wrap=tk.WORD, width=80, height=20)
        self.progress_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        self.status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

        # 处理状态
        self.processing = False

    def create_menu_bar(self):
        """创建菜单栏"""
        menu_bar = tk.Menu(self.root)
        self.root.config(menu=menu_bar)
        
        # 文件菜单
        file_menu = tk.Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label="文件", menu=file_menu)
        file_menu.add_command(label="退出", command=self.root.quit)
        
        # 帮助菜单
        help_menu = tk.Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label="帮助", menu=help_menu)
        help_menu.add_command(label="使用说明", command=self.show_user_guide)
        help_menu.add_command(label="版本信息", command=self.show_version_info)
        help_menu.add_command(label="关于", command=self.show_about)
    
    def show_user_guide(self):
        """显示使用说明"""
        guide_window = tk.Toplevel(self.root)
        guide_window.title("使用说明")
        guide_window.geometry("600x500")
        guide_window.resizable(True, True)
        
        # 使用说明内容
        user_guide = """
文档文本替换工具使用说明

基本功能
本工具用于批量替换文件夹中的DOC和DOCX文档里的文字，支持保留原文本格式。

操作步骤
1. **选择文件夹**：点击"浏览"按钮，选择包含要处理文档的文件夹。
2. **设置替换文本**：
   - 在"查找文本"输入框中输入要查找的文本。
   - 在"替换为"输入框中输入要替换成的文本。
3. **选择文件类型**：勾选要处理的文件类型（DOCX和/或DOC）。
4. **开始替换**：点击"开始替换"按钮，程序将开始处理文件。
5. **查看进度**：在处理进度区域可以实时查看处理情况。

注意事项
- **关于DOC文件**：处理DOC文件需要安装Microsoft Word应用程序。如果您只有WPS Office，请不要勾选DOC类型文档。
- **文本替换范围**：工具会替换文档中的所有文本，包括正文、表格、页眉页脚和文本框中的内容。
- **格式保留**：替换过程会保留原文本的格式（如字体、大小、颜色等）。
- **处理时间**：处理大量文件可能需要较长时间，请耐心等待。
- **备份建议**：为避免意外情况，建议在处理重要文档前先备份原文件。
        """
        
        # 创建滚动文本框
        text_area = scrolledtext.ScrolledText(guide_window, wrap=tk.WORD, width=70, height=25)
        text_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
        text_area.insert(tk.END, user_guide)
        text_area.config(state=tk.DISABLED)  # 设置为只读
        
        # 关闭按钮
        close_button = tk.Button(guide_window, text="关闭", command=guide_window.destroy)
        close_button.pack(pady=10)
    
    def show_version_info(self):
        """显示版本信息"""
        version_window = tk.Toplevel(self.root)
        version_window.title("版本信息")
        version_window.geometry("400x300")
        version_window.resizable(False, False)
        
        # 版本信息内容
        version_frame = tk.Frame(version_window, padx=20, pady=20)
        version_frame.pack(fill=tk.BOTH, expand=True)
        
        tk.Label(version_frame, text=APP_NAME, font=("Arial", 16, "bold")).pack(pady=(0, 10))
        tk.Label(version_frame, text=f"版本: {APP_VERSION}").pack(pady=5)

        # 版本历史
        history_frame = tk.LabelFrame(version_frame, text="版本历史")
        history_frame.pack(pady=10, fill=tk.X)
        
        version_history = """
v1.0.0 (2023-12-01)
- 初始版本发布
- 支持批量替换DOC和DOCX文档中的文本
- 支持保留原文本格式
- 支持处理页眉页脚和文本框中的内容
        """
        
        history_text = scrolledtext.ScrolledText(history_frame, wrap=tk.WORD, width=40, height=8)
        history_text.pack(padx=5, pady=5, fill=tk.BOTH)
        history_text.insert(tk.END, version_history)
        history_text.config(state=tk.DISABLED)  # 设置为只读
        
        # 关闭按钮
        close_button = tk.Button(version_frame, text="关闭", command=version_window.destroy)
        close_button.pack(pady=10)
    
    def show_about(self):
        """显示关于信息"""
        about_window = tk.Toplevel(self.root)
        about_window.title("关于")
        about_window.geometry("400x300")
        about_window.resizable(False, False)
        
        # 关于信息内容
        about_frame = tk.Frame(about_window, padx=20, pady=20)
        about_frame.pack(fill=tk.BOTH, expand=True)
        
        tk.Label(about_frame, text=APP_NAME, font=("Arial", 16, "bold")).pack(pady=(0, 10))
        tk.Label(about_frame, text=APP_DESCRIPTION).pack(pady=10)
        tk.Label(about_frame, text=f"开发者: {APP_AUTHOR}").pack(pady=5)

        # 联系信息
        contact_frame = tk.LabelFrame(about_frame, text="联系方式")
        contact_frame.pack(pady=10, fill=tk.X)
        
        tk.Label(contact_frame, text="邮箱: chenliangxu68@163.com").pack(pady=2)
        
        # 关闭按钮
        close_button = tk.Button(about_frame, text="关闭", command=about_window.destroy)
        close_button.pack(pady=10)

    def browse_folder(self):
        folder_selected = filedialog.askdirectory()
        if folder_selected:
            self.folder_path.set(folder_selected)

    def log(self, message):
        self.progress_text.insert(tk.END, message + "\n")
        self.progress_text.see(tk.END)
        self.root.update_idletasks()

    def start_replace(self):
        if self.processing:
            messagebox.showwarning("警告", "正在处理中，请等待完成")
            return

        folder_path = self.folder_path.get()
        find_text = self.find_text.get()
        replace_text = self.replace_text.get()

        if not folder_path or not os.path.isdir(folder_path):
            messagebox.showerror("错误", "请选择有效的文件夹")
            return

        if not find_text:
            messagebox.showerror("错误", "请输入要查找的文本")
            return

        # 清空进度显示
        self.progress_text.delete(1.0, tk.END)

        # 在新线程中执行替换操作
        self.processing = True
        self.status_var.set("处理中...")
        threading.Thread(target=self.replace_in_docs, args=(folder_path, find_text, replace_text), daemon=True).start()

    def replace_text_in_runs(self, runs, find_text, replace_text):
        """在保留格式的情况下替换runs中的文本"""
        if not runs:
            return False
            
        # 获取所有runs的文本
        text = "".join([run.text for run in runs])
        
        # 如果没有找到要替换的文本，直接返回
        if find_text not in text:
            return False
            
        # 处理文本替换
        # 这种方法处理跨多个run的文本替换
        start_idx = 0
        runs_text = []
        runs_mapping = []
        
        # 构建runs的映射关系
        for i, run in enumerate(runs):
            if not run.text:
                continue
                
            end_idx = start_idx + len(run.text)
            runs_text.append(run.text)
            runs_mapping.append((start_idx, end_idx, i))
            start_idx = end_idx
            
        full_text = "".join(runs_text)
        new_text = full_text.replace(find_text, replace_text)
        
        # 如果文本没有变化，直接返回
        if full_text == new_text:
            return False
            
        # 重新分配文本到各个run，保留格式
        current_position = 0
        for start_idx, end_idx, run_idx in runs_mapping:
            run_length = end_idx - start_idx
            if current_position + run_length <= len(new_text):
                runs[run_idx].text = new_text[current_position:current_position + run_length]
            else:
                # 处理最后一个run可能超出新文本长度的情况
                if current_position < len(new_text):
                    runs[run_idx].text = new_text[current_position:]
                else:
                    runs[run_idx].text = ""
            current_position += run_length
        
        return True
        
    def replace_in_shapes_and_textboxes(self, doc, find_text, replace_text):
        """处理文本框和形状中的文本"""
        try:
            # 获取文档中的所有形状和文本框
            # 这需要使用python-docx的底层XML操作
            from docx.oxml.ns import qn
            
            replaced = False
            
            # 文本框通常在drawing元素中
            drawings = doc._body._element.findall('.//'+qn('w:drawing'))
            
            for drawing in drawings:
                # 查找文本框中的文本内容
                txbx_contents = drawing.findall('.//'+qn('w:txbxContent'))
                for txbx_content in txbx_contents:
                    # 处理文本框中的段落
                    for p in txbx_content.findall('.//'+qn('w:p')):
                        # 获取段落中的所有文本
                        text_elements = p.findall('.//'+qn('w:t'))
                        paragraph_text = ''.join([t.text for t in text_elements if t.text])
                        
                        if find_text in paragraph_text:
                            # 处理段落中的所有文本运行
                            for r in p.findall('.//'+qn('w:r')):
                                t_elements = r.findall('.//'+qn('w:t'))
                                for t in t_elements:
                                    if t.text and find_text in t.text:
                                        t.text = t.text.replace(find_text, replace_text)
                                        replaced = True
            
            return replaced
        except Exception as e:
            print(f"处理形状和文本框时出错: {str(e)}")
            return False
    
    def replace_in_docx(self, file_path, find_text, replace_text):
        """处理.docx格式文件，保留文本格式"""
        try:
            # 打开文档
            doc = Document(file_path)
            replaced = False

            # 替换段落中的文本
            for para in doc.paragraphs:
                if find_text in para.text:
                    if self.replace_text_in_runs(para.runs, find_text, replace_text):
                        replaced = True
                        self.log(f"  - 已替换段落中的文本")

            # 替换表格中的文本
            for table in doc.tables:
                for row in table.rows:
                    for cell in row.cells:
                        for paragraph in cell.paragraphs:
                            if find_text in paragraph.text:
                                if self.replace_text_in_runs(paragraph.runs, find_text, replace_text):
                                    replaced = True
                                    self.log(f"  - 已替换表格中的文本")
            
            # 替换页眉页脚中的文本
            for section in doc.sections:
                # 处理页眉
                for paragraph in section.header.paragraphs:
                    if find_text in paragraph.text:
                        if self.replace_text_in_runs(paragraph.runs, find_text, replace_text):
                            replaced = True
                            self.log(f"  - 已替换页眉中的文本")
                
                # 处理页脚
                for paragraph in section.footer.paragraphs:
                    if find_text in paragraph.text:
                        if self.replace_text_in_runs(paragraph.runs, find_text, replace_text):
                            replaced = True
                            self.log(f"  - 已替换页脚中的文本")
            
            # 替换文本框和形状中的文本
            try:
                if self.replace_in_shapes_and_textboxes(doc, find_text, replace_text):
                    replaced = True
                    self.log(f"  - 已替换文本框/形状中的文本")
            except Exception as e:
                self.log(f"  - 处理文本框和形状时出错: {str(e)}")

            # 如果有替换，保存文档
            if replaced:
                doc.save(file_path)
                self.log(f"  - 文档已保存")
                return True
            else:
                self.log(f"  - 未找到匹配文本")
                return False
        except Exception as e:
            self.log(f"  - 处理.docx文件失败: {str(e)}")
            return False

    def replace_in_doc(self, file_path, find_text, replace_text):
        """处理.doc格式文件，保留文本格式并处理所有文本容器"""
        try:
            # 初始化COM
            pythoncom.CoInitialize()

            # 创建Word应用程序对象
            word = win32com.client.Dispatch("Word.Application")
            word.Visible = False

            try:
                # 打开文档
                doc = word.Documents.Open(file_path)

                # 执行查找和替换
                find_obj = word.Selection.Find
                find_obj.ClearFormatting()
                find_obj.Text = find_text
                # 不清除替换文本的格式，这样可以保留原文本的格式
                # find_obj.Replacement.ClearFormatting()  # 注释掉这一行
                find_obj.Replacement.Text = replace_text
                
                # 设置格式保留选项
                # 0 = wdReplaceNone, 1 = wdReplaceOne, 2 = wdReplaceAll
                wdReplaceAll = 2
                
                # 设置搜索范围包括主文档、页眉页脚、文本框等
                wdMainTextStory = 1  # 主文档
                wdHeaderStory = 6    # 页眉
                wdFooterStory = 7    # 页脚
                wdTextFrameStory = 5 # 文本框
                
                replaced = False
                
                # 在主文档中搜索替换
                word.Selection.HomeKey(Unit=6)  # 6 = wdStory，移动到文档开始
                if find_obj.Execute(FindText=find_text, ReplaceWith=replace_text,
                                   Forward=True, Wrap=1, MatchCase=False,
                                   MatchWholeWord=False, MatchWildcards=False,
                                   MatchSoundsLike=False, MatchAllWordForms=False,
                                   Format=True, Replace=wdReplaceAll):
                    replaced = True
                    self.log(f"  - 已替换主文档中的文本")
                
                # 在页眉中搜索替换
                for i in range(1, doc.Sections.Count + 1):
                    section = doc.Sections(i)
                    for j in range(1, 4):  # 处理不同类型的页眉
                        try:
                            header = section.Headers(j)
                            word.Selection.HomeKey(Unit=6)
                            if find_obj.Execute(FindText=find_text, ReplaceWith=replace_text,
                                              Forward=True, Wrap=1, MatchCase=False,
                                              MatchWholeWord=False, MatchWildcards=False,
                                              MatchSoundsLike=False, MatchAllWordForms=False,
                                              Format=True, Replace=wdReplaceAll):
                                replaced = True
                                self.log(f"  - 已替换页眉中的文本")
                        except:
                            pass
                
                # 在页脚中搜索替换
                for i in range(1, doc.Sections.Count + 1):
                    section = doc.Sections(i)
                    for j in range(1, 4):  # 处理不同类型的页脚
                        try:
                            footer = section.Footers(j)
                            word.Selection.HomeKey(Unit=6)
                            if find_obj.Execute(FindText=find_text, ReplaceWith=replace_text,
                                              Forward=True, Wrap=1, MatchCase=False,
                                              MatchWholeWord=False, MatchWildcards=False,
                                              MatchSoundsLike=False, MatchAllWordForms=False,
                                              Format=True, Replace=wdReplaceAll):
                                replaced = True
                                self.log(f"  - 已替换页脚中的文本")
                        except:
                            pass
                
                # 在文本框和形状中搜索替换
                try:
                    # 遍历所有形状
                    for i in range(1, doc.Shapes.Count + 1):
                        shape = doc.Shapes(i)
                        # 检查形状是否有文本范围
                        if hasattr(shape, "TextFrame") and hasattr(shape.TextFrame, "TextRange"):
                            text_range = shape.TextFrame.TextRange
                            text_range.Find.ClearFormatting()
                            text_range.Find.Text = find_text
                            text_range.Find.Replacement.Text = replace_text
                            if text_range.Find.Execute(FindText=find_text, ReplaceWith=replace_text,
                                                     Forward=True, Wrap=1, MatchCase=False,
                                                     MatchWholeWord=False, MatchWildcards=False,
                                                     MatchSoundsLike=False, MatchAllWordForms=False,
                                                     Format=True, Replace=wdReplaceAll):
                                replaced = True
                                self.log(f"  - 已替换形状/文本框中的文本")
                except Exception as e:
                    self.log(f"  - 处理形状和文本框时出错: {str(e)}")

                # 保存并关闭文档
                doc.Save()
                doc.Close()

                if replaced:
                    self.log(f"  - 文档已保存")
                    return True
                else:
                    self.log(f"  - 未找到匹配文本")
                    return False
            finally:
                # 退出Word应用程序
                word.Quit()
                # 释放COM资源
                pythoncom.CoUninitialize()
        except Exception as e:
            self.log(f"  - 处理.doc文件失败: {str(e)}")
            return False

    def replace_in_docs(self, folder_path, find_text, replace_text):
        try:
            start_time = time.time()
            self.log(f"开始处理文件夹: {folder_path}")
            self.log(f"查找文本: '{find_text}'")
            self.log(f"替换为: '{replace_text}'")
            self.log("----------------------------")

            total_files = 0
            processed_files = 0
            replaced_files = 0

            # 获取用户选择的文件类型
            process_docx = self.docx_var.get()
            process_doc = self.doc_var.get()
            
            # 构建文件扩展名列表
            extensions = []
            if process_docx:
                extensions.append('.docx')
                self.log("将处理 DOCX 文件")
            if process_doc:
                extensions.append('.doc')
                self.log("将处理 DOC 文件")
                
            if not extensions:
                self.log("警告: 未选择任何文件类型，无法进行处理")
                self.status_var.set("未选择任何文件类型")
                return
                
            # 遍历文件夹
            for root, dirs, files in os.walk(folder_path):
                for file in files:
                    file_lower = file.lower()
                    # 只处理用户选择的文件类型
                    if any(file_lower.endswith(ext) for ext in extensions):
                        total_files += 1
                        file_path = os.path.join(root, file)
                        self.log(f"处理文件: {file_path}")

                        replaced = False
                        if file_lower.endswith('.docx') and process_docx:
                            replaced = self.replace_in_docx(file_path, find_text, replace_text)
                        elif file_lower.endswith('.doc') and process_doc:
                            replaced = self.replace_in_doc(file_path, find_text, replace_text)

                        if replaced:
                            replaced_files += 1

                        processed_files += 1

            elapsed_time = time.time() - start_time
            self.log("----------------------------")
            self.log(f"处理完成! 耗时: {elapsed_time:.2f}秒")
            self.log(f"总文件数: {total_files}")
            self.log(f"处理文件数: {processed_files}")
            self.log(f"替换文件数: {replaced_files}")

            self.status_var.set(f"完成 - 处理了 {processed_files} 个文件，替换了 {replaced_files} 个文件")
        except Exception as e:
            self.log(f"发生错误: {str(e)}")
            self.status_var.set("处理出错")
        finally:
            self.processing = False

def main():
    try:
        root = tk.Tk()
        app = DocReplacer(root)
        root.mainloop()
    except Exception as e:
        import traceback
        error_msg = f"发生错误: {str(e)}\n{traceback.format_exc()}"
        print(error_msg)
        
        # 尝试将错误写入日志文件
        try:
            with open("replacer_error.log", "w") as f:
                f.write(error_msg)
        except:
            pass
        
        # 如果GUI已经创建，显示错误消息框
        try:
            messagebox.showerror("错误", f"程序启动失败: {str(e)}\n详细信息已写入replacer_error.log文件")
        except:
            pass

if __name__ == "__main__":
    main()
